const logger = require('log4js').getLogger(),
    net = require('net'),
    config = require('./config')

exports.startProxyServer = obj => {
    obj = obj || {}
    obj.exPort = obj.exPort || 8080
    obj.inPort = obj.inPort || 18080
    var inClients = []
    var exClients = []

    var exServer = net.createServer(exClient => {
		for (var priority = 0; priority < inClients.length; priority++) {
			if (inClients[priority].length == 0)
				continue
			var idx = require('../../lib/random')(0, inClients[priority].length - 1)
			for(var i = 0; i < inClients[priority].length; i++) {
				if (inClients[priority][idx] != null)
					break
				idx = (idx + 1) % inClients[priority].length
			}
			if (inClients[priority][idx] != null)
				break
		}
		if (priority == inClients.length) {
			logger.warn('[' + __filename + ':' + __line + ']', obj.exPort, 'inClients == null')
			exClient.end()
			return
		}
        let clientId = exClients.length
        for (var i = 0; i < exClients.length; i++) {
            if (exClients[i] == null) {
                clientId = i
                break
            }
        }
        if (clientId != exClients.length)
            exClients[clientId] = exClient
        else
            exClients.push(exClient)
        //logger.debug('[' + __filename + ':' + __line + ']', 'exClients[' + clientId + '] connected, inClients[' + priority + '][' + idx + '] allocated')
        exClient.on('data', data => {
            //logger.debug('[' + __filename + ':' + __line + ']', 'exClient on data')
			if (inClients[priority][idx] == null) {
				exClient.end()
			}
            inSend(inClients[priority][idx], clientId, config.ProxyDataType.DATA, data)
        })
        exClient.on('end', () => {
            //logger.debug('[' + __filename + ':' + __line + ']', 'exClients[' + clientId+ '] on end')
        })
        exClient.on('error', err => {
            logger.error('[' + __filename + ':' + __line + ']', 'exClients[' + clientId + ']', JSON.stringify(err))
        })
        exClient.on('close', () => {
            //logger.debug('[' + __filename + ':' + __line + ']', 'exClients[' + clientId+ '] on close')
            inSend(inClients[priority][idx], clientId, config.ProxyDataType.CLOSE)
            exClients[clientId] = null
        })
        exClient.setTimeout(config.proxyIdleTimeout)
        exClient.on('timeout', () => {
            logger.debug('[' + __filename + ':' + __line + ']', 'exClients[' + clientId + '] on timeout')
            try {
                exClient.end()
            } catch (err) {
                logger.debug('[' + __filename + ':' + __line + ']', 'exClients[' + clientId + ']', err)
                inSend(inClients[idx], clientId, config.ProxyDataType.CLOSE)
                exClients[clientId] = null
            }
        })
    })
    exServer.on('error', err => {
        logger.error('[' + __filename + ':' + __line + ']', err)
    })
    exServer.listen(obj.exPort)

    var inServer = net.createServer(inClient => {
        var idx = -1
        var priority = -1
        var recvData = Buffer.alloc(0)
        inClient.on('data', data => {
            // logger.debug('[' + __filename + ':' + __line + ']', 'inClient on data', data)
            var newData = Buffer.alloc(recvData.length + data.length)
            recvData.copy(newData)
            data.copy(newData, recvData.length)
            data = newData
            var offset = 0
            while (offset < data.length) {
                if (data.length - offset < config.proxyDataLengthByteNum) {
                    break
                } else {
                    var len = 0
                    for (var i = 0; i < config.proxyDataLengthByteNum; i++)
                        len += data[offset + i] * (1 << ((config.proxyDataLengthByteNum - i - 1) * 8))

                    if (data.length - offset < len + config.proxyDataLengthByteNum) {
                        break
                    } else {
                        var preLen = config.proxyDataLengthByteNum
                        var clientId = 0
                        for (var i = 0; i < config.proxyClientIdByteNum; i++)
                            clientId += data[offset + preLen + i] * (1 << ((config.proxyClientIdByteNum - i - 1) * 8))

                        preLen += config.proxyClientIdByteNum
                        var dataType = 0
                        for (var i = 0; i < config.proxyDataTypeByteNum; i++)
                            dataType += data[offset + preLen + i] * (1 << ((config.proxyDataTypeByteNum - i - 1) * 8))
                        
                        if (dataType == config.ProxyDataType.CLOSE) {
							if (exClients[clientId] != null)
								exClients[clientId].end()
                        } else if (dataType == config.ProxyDataType.HELLO) {
                            // logger.debug('[' + __filename + ':' + __line + ']', 'inClients[' + priority + '][' + idx + '] recv hello')
						} else if (dataType == config.ProxyDataType.CONNECT) {
                            preLen += config.proxyDataTypeByteNum
							if (priority != -1 && idx != -1)
								inClients[priority][idx] = null
							priority = 0
							for (var i = offset + preLen; i < offset + config.proxyDataLengthByteNum + len; i++)
								priority = (priority << 8) + data[i]
							while(inClients.length <= priority)
								inClients.push([])
							idx = inClients[priority].length
							for (var i = 0; i < inClients[priority].length; i++) {
								if (inClients[priority][i] == null) {
									idx = i
									break
								}
							}
							if (idx != inClients[priority].length)
								inClients[priority][idx] = inClient
							else
								inClients[priority].push(inClient)
							logger.debug('[' + __filename + ':' + __line + ']', 'inClients[' + priority + '][' + idx + '] connected', inClient.address())
                        } else {
							if (exClients[clientId] != null) {
                                preLen += config.proxyDataTypeByteNum
                                exClients[clientId].write(data.slice(offset + preLen, offset + config.proxyDataLengthByteNum + len))
                            }
                        }
                        offset += len + config.proxyDataLengthByteNum
                    }
                }
            }
            recvData = data.slice(offset)
        })
        inClient.on('end', () => {
            //logger.debug('[' + __filename + ':' + __line + ']', 'inClients[' + priority + '][' + idx + '] on end')
        })
        inClient.on('error', err => {
			if (priority != -1 && idx != -1) {
				logger.error('[' + __filename + ':' + __line + ']', 'inClients[' + priority + '][' + idx + ']', JSON.stringify(err))
			}
        })
        inClient.on('close', () => {
			if (priority != -1 && idx != -1) {
				logger.debug('[' + __filename + ':' + __line + ']', 'inClients[' + priority + '][' + idx + '] on close')
				inClients[priority][idx] = null
			}
			clearInterval(helloInterval)
        })
        inClient.setTimeout(config.proxyIdleTimeout)
        inClient.on('timeout', () => {
			if (priority != -1 && idx != -1) {
				logger.debug('[' + __filename + ':' + __line + ']', 'inClients[' + priority + '][' + idx + '] on timeout')
			}
            inClient.end()
        })
        let helloInterval = setInterval(() => {
            //logger.debug('[' + __filename + ':' + __line + ']', 'inClients[' + priority + '][' + idx + '] send hello')
            inSend(inClient, 0, config.ProxyDataType.HELLO)
        }, config.proxyHelloTimeout)
    })
    inServer.on('error', err => {
        logger.error('[' + __filename + ':' + __line + ']', err)
    })
    inServer.listen(obj.inPort)
}

exports.startProxyClient = obj => {
    obj = obj || {}
	obj.priority = obj.priority || config.proxyDefaultPriority
    obj.port = obj.port || 18080
    var endClients = []
    var inClient = net.createConnection({
        port: obj.port,
        host: '123.56.115.107'
    }, () => {
        logger.debug('[' + __filename + ':' + __line + ']', 'inClient on connect')
		var len = 1
		while(obj.priority >> (len * 8))
			len++
		var data = Buffer.alloc(len)
		for (var i = 0; i < len; i++)
			data[len - 1 - i] = (obj.priority >> (i * 8)) & 0xff
		inSend(inClient, 0, config.ProxyDataType.CONNECT, data)
    })
    var recvData = Buffer.alloc(0)
    inClient.on('data', (data) => {
        //logger.debug('[' + __filename + ':' + __line + ']', 'inClient on data')
        var newData = Buffer.alloc(recvData.length + data.length)
        recvData.copy(newData)
        data.copy(newData, recvData.length)
        data = newData
        var offset = 0
        while (offset < data.length) {
            if (data.length - offset < config.proxyDataLengthByteNum) {
                break
            } else {
                let len = 0
                for (var i = 0; i < config.proxyDataLengthByteNum; i++) {
                    len += data[offset + i] * (1 << ((config.proxyDataLengthByteNum - i - 1) * 8))
                }

                if (data.length - offset < len + config.proxyDataLengthByteNum) {
                    break
                } else {
                    var preLen = config.proxyDataLengthByteNum
                    let clientId = 0
                    for (var i = 0; i < config.proxyClientIdByteNum; i++)
                        clientId += data[offset + preLen + i] * (1 << ((config.proxyClientIdByteNum - i - 1) * 8))
                    while (endClients.length <= clientId)
                        endClients.push(null)

                    preLen += config.proxyClientIdByteNum
                    var dataType = 0
                    for (var i = 0; i < config.proxyDataTypeByteNum; i++)
                        dataType += data[offset + preLen + i] * (1 << ((config.proxyDataTypeByteNum - i - 1) * 8))

                    if (dataType == config.ProxyDataType.CLOSE) {
                        if (endClients[clientId] != null)
                            endClients[clientId].end()
                    } else if (dataType == config.ProxyDataType.HELLO) {
                        //logger.debug('[' + __filename + ':' + __line + ']', 'inClient recv hello')
                    } else {
                        preLen += config.proxyDataTypeByteNum
                        if (endClients[clientId] != null) {
                            endClients[clientId].write(data.slice(offset + preLen, offset + config.proxyDataLengthByteNum + len))
                        } else {
                            let start = offset + preLen
                            let end = offset + config.proxyDataLengthByteNum + len
                            let dataLength = data.length
                            endClients[clientId] = net.createConnection({
                                port: 9080
                            }, () => {
                                if (data.length != dataLength) {
                                    logger.error('[' + __filename + ':' + __line + ']', 'data.length=' + data.length, 'dataLength=' + dataLength)
                                    return
                                }
                                endClients[clientId].write(data.slice(start, end))
                            })
                            endClients[clientId].on('data', (data) => {
                                //logger.debug('[' + __filename + ':' + __line + ']', 'endClients[' + clientId + '] on data')
                                inSend(inClient, clientId, config.ProxyDataType.DATA, data)
                            })
                            endClients[clientId].on('end', () => {
                                //logger.debug('[' + __filename + ':' + __line + ']', 'endClients[' + clientId + '] on end')
                            })
                            endClients[clientId].on('error', err => {
                                logger.error('[' + __filename + ':' + __line + ']', 'endClients[' + clientId + ']', err)
                            })
                            endClients[clientId].on('close', () => {
                                //logger.debug('[' + __filename + ':' + __line + ']', 'endClients[' + clientId + '] on close')
                                inSend(inClient, clientId, config.ProxyDataType.CLOSE)
                                endClients[clientId] = null
                            })
                            endClients[clientId].setTimeout(config.proxyIdleTimeout)
                            endClients[clientId].on('timeout', () => {
                                logger.debug('[' + __filename + ':' + __line + ']', 'endClients[' + clientId + '] on timeout')
                                endClients[clientId].end()
                            })
                        }
                    }
                    offset += len + config.proxyDataLengthByteNum
                }
            }
        }
        recvData = data.slice(offset)
    })
    inClient.on('end', () => {
        logger.debug('[' + __filename + ':' + __line + ']', 'inClient on end')
    })
    inClient.on('error', err => {
        logger.error('[' + __filename + ':' + __line + ']', err)
    })
    inClient.on('close', () => {
        logger.debug('[' + __filename + ':' + __line + ']', 'inClient on close')
        clearInterval(helloInterval)
        setTimeout(() => {
            exports.startProxyClient(obj)
        }, config.proxyReconnectTimeout)
    })
    inClient.setTimeout(config.proxyIdleTimeout)
    inClient.on('timeout', () => {
        logger.debug('[' + __filename + ':' + __line + ']', 'inClient on timeout')
        inClient.end()
    })
    let helloInterval = setInterval(() => {
        //logger.debug('[' + __filename + ':' + __line + ']', 'inClient send hello')
        inSend(inClient, 0, config.ProxyDataType.HELLO)
    }, config.proxyHelloTimeout)
}

function inSend(inClient, clientId, type, data) {
    if (inClient == null)
        return

    data = data || Buffer.alloc(0)
    var offset = 0
    while (offset < data.length || data.length == 0) {
        if (config.proxyDataLengthByteNum == 0) {
            logger.error('[' + __filename + ':' + __line + ']', 'config.proxyDataLengthByteNum == 0')
            return
        }
        var len = data.length - offset + config.proxyClientIdByteNum + config.proxyDataTypeByteNum
        if ((len >> (config.proxyDataLengthByteNum * 8)) > 0)
            len = (1 << (config.proxyDataLengthByteNum * 8)) - 1
        var dataLengthBuffer = Buffer.alloc(config.proxyDataLengthByteNum)
        for (var i = 0; i < config.proxyDataLengthByteNum; i++) {
            dataLengthBuffer[config.proxyDataLengthByteNum - i - 1] = (len >> (i * 8)) & 0xff
        }
        inClient.write(dataLengthBuffer)
        // logger.debug('[' + __filename + ':' + __line + ']', 'inClient send', dataLengthBuffer)

        if (config.proxyClientIdByteNum == 0 || (clientId >> (config.proxyClientIdByteNum * 8)) > 0) {
            logger.error('[' + __filename + ':' + __line + ']', 'config.proxyClientIdByteNum wrong')
            return
        }
        var clientIdBuffer = Buffer.alloc(config.proxyClientIdByteNum)
        for (var i = 0; i < config.proxyClientIdByteNum; i++)
            clientIdBuffer[config.proxyClientIdByteNum - i - 1] = (clientId >> (i * 8)) & 0xff
        inClient.write(clientIdBuffer)
        // logger.debug('[' + __filename + ':' + __line + ']', 'inClient send', clientIdBuffer)

        var dataTypeBuffer = Buffer.alloc(config.proxyDataTypeByteNum)
        for (var i = 0; i < config.proxyDataTypeByteNum; i++)
            dataTypeBuffer[config.proxyDataTypeByteNum - i - 1] = (type >> (i * 8)) & 0xff
        inClient.write(dataTypeBuffer)
        // logger.debug('[' + __filename + ':' + __line + ']', 'inClient send', dataTypeBuffer)

        if (data.length == 0)
            break
        inClient.write(data.slice(offset, offset + len - config.proxyClientIdByteNum - config.proxyDataTypeByteNum))

        offset += len - config.proxyClientIdByteNum - config.proxyDataTypeByteNum
    }
}

function setSocketTimeout(skt, time, callback) {
    skt.on('timeout', () => {
        skt.removeAllListeners('end')
        skt.end()
        callback()
    })
    skt.setTimeout(time)
}